<file alias="icons/scalable/status/screenshot-ui-area-symbolic.svg">screenshot-ui-area-symbolic.svg</file>
<file alias="icons/scalable/status/screenshot-ui-display-symbolic.svg">screenshot-ui-display-symbolic.svg</file>
<file alias="icons/scalable/status/screenshot-ui-window-symbolic.svg">screenshot-ui-window-symbolic.svg</file>
+ <file alias="icons/scalable/status/select-mode-symbolic.svg">select-mode-symbolic.svg</file>
</gresource>
</gresources>
}
}
+.screenshot-ui-show-pointer-button {
+ padding: $base_padding * 2;
+ border-radius: 99px;
+ background-color: $hover_bg_color;
+ &:hover, &:focus { background-color: lighten($hover_bg_color, 5%); }
+ &:active { background-color: $active_bg_color; }
+ &:checked { background-color: darken($hover_bg_color, 10%); }
+
+ StIcon {
+ icon-size: $base_icon_size;
+ }
+}
+
.screenshot-ui-area-indicator-shade {
background-color: rgba(0, 0, 0, .3);
}
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" height="16px" viewBox="0 0 16 16" width="16px"><path d="m 2.953125 1.074219 l 2.417969 13.210937 l 3.238281 -2.398437 l 2.054687 2.648437 c 1.03125 1.433594 3.148438 -0.210937 2.011719 -1.5625 l -2.015625 -2.59375 l 2.984375 -2.175781 z m 0 0" fill="#2e3436"/></svg>
y_align: Clutter.ActorAlign.CENTER,
});
+ this._cursor = null;
+ this._cursorPoint = { x: 0, y: 0 };
+ this._shouldShowCursor = actor.get_children().some(c => c.has_pointer);
+
this.connect('destroy', this._onDestroy.bind(this));
}
return [0, 0];
}
+ get cursorPoint() {
+ return {
+ x: this._cursorPoint.x + this._boundingBox.x - this._bufferRect.x,
+ y: this._cursorPoint.y + this._boundingBox.y - this._bufferRect.y,
+ };
+ }
+
get bufferScale() {
return this._bufferScale;
}
this.remove_child(this._border);
this._border.destroy();
this._border = null;
+
+ if (this._cursor) {
+ this.remove_child(this._cursor);
+ this._cursor.destroy();
+ this._cursor = null;
+ }
}
vfunc_allocate(box) {
windowH * yScale / this._bufferScale
);
this._actor.allocate(actorBox);
+
+ // Allocate the cursor if we have one.
+ if (!this._cursor)
+ return;
+
+ let [, , w, h] = this._cursor.get_preferred_size();
+ w *= this._cursorScale;
+ h *= this._cursorScale;
+
+ const cursorBox = new Clutter.ActorBox({
+ x1: this._cursorPoint.x,
+ y1: this._cursorPoint.y,
+ x2: this._cursorPoint.x + w,
+ y2: this._cursorPoint.y + h,
+ });
+ cursorBox.x1 *= xScale;
+ cursorBox.x2 *= xScale;
+ cursorBox.y1 *= yScale;
+ cursorBox.y2 *= yScale;
+
+ this._cursor.allocate(cursorBox);
+ }
+
+ addCursorTexture(content, point, scale) {
+ if (!this._shouldShowCursor)
+ return;
+
+ // Add the cursor.
+ this._cursor = new St.Widget({
+ content,
+ request_mode: Clutter.RequestMode.CONTENT_SIZE,
+ });
+
+ this._cursorPoint = {
+ x: point.x - this._boundingBox.x,
+ y: point.y - this._boundingBox.y,
+ };
+ this._cursorScale = scale;
+
+ this.insert_child_below(this._cursor, this._border);
+ }
+
+ setCursorVisible(visible) {
+ if (!this._cursor)
+ return;
+
+ this._cursor.visible = visible;
}
});
}));
this._stageScreenshotContainer.add_child(this._stageScreenshot);
+ this._cursor = new St.Widget();
+ this._stageScreenshotContainer.add_child(this._cursor);
+
this._openingCoroutineInProgress = false;
this._grabHelper = new GrabHelper.GrabHelper(this, {
actionMode: Shell.ActionMode.POPUP,
this._onCaptureButtonClicked.bind(this));
this._bottomRowContainer.add_child(this._captureButton);
+ this._showPointerButtonContainer = new St.BoxLayout({
+ x_align: Clutter.ActorAlign.END,
+ x_expand: true,
+ });
+ this._bottomRowContainer.add_child(this._showPointerButtonContainer);
+
+ this._showPointerButton = new St.Button({
+ style_class: 'screenshot-ui-show-pointer-button',
+ toggle_mode: true,
+ });
+ this._showPointerButton.set_child(new St.Icon({ icon_name: 'select-mode-symbolic' }));
+ this._showPointerButtonContainer.add_child(this._showPointerButton);
+
+ this._showPointerButton.connect('notify::checked', () => {
+ const state = this._showPointerButton.checked;
+ this._cursor.visible = state;
+
+ const windows =
+ this._windowSelectors.flatMap(selector => selector.windows());
+ for (const window of windows)
+ window.setCursorVisible(state);
+ });
+ this._cursor.visible = false;
+
this._monitorBins = [];
this._windowSelectors = [];
this._rebuildMonitorBins();
this._openingCoroutineInProgress = true;
try {
- const [content, scale] =
+ const [content, scale, cursorContent, cursorPoint, cursorScale] =
await this._shooter.screenshot_stage_to_content();
this._stageScreenshot.set_content(content);
this._scale = scale;
+ if (cursorContent !== null) {
+ this._cursor.set_content(cursorContent);
+ this._cursor.set_position(cursorPoint.x, cursorPoint.y);
+
+ let [, w, h] = cursorContent.get_preferred_size();
+ w *= cursorScale;
+ h *= cursorScale;
+ this._cursor.set_size(w, h);
+
+ this._cursorScale = cursorScale;
+
+ for (const window of windows) {
+ window.addCursorTexture(cursorContent, cursorPoint, cursorScale);
+ window.setCursorVisible(this._showPointerButton.checked);
+ }
+ }
+
this._stageScreenshotContainer.show();
} catch (e) {
log('Error capturing screenshot: %s'.format(e.message));
this._stageScreenshotContainer.hide();
this._stageScreenshot.set_content(null);
+ this._cursor.set_content(null);
this._areaSelector.reset();
for (const selector of this._windowSelectors)
const [x, y, w, h] = this._getSelectedGeometry();
+ let cursorTexture = this._cursor.content?.get_texture();
+ if (!this._cursor.visible)
+ cursorTexture = null;
+
Shell.Screenshot.composite_to_stream(
texture,
x, y, w, h,
+ this._scale,
+ cursorTexture ?? null,
+ this._cursor.x * this._scale,
+ this._cursor.y * this._scale,
+ this._cursorScale,
stream
).then(() => {
stream.close(null);
const texture = content.get_texture();
const stream = Gio.MemoryOutputStream.new_resizable();
+ let cursorTexture = this._cursor.content?.get_texture();
+ if (!this._cursor.visible)
+ cursorTexture = null;
+
Shell.Screenshot.composite_to_stream(
texture,
0, 0, -1, -1,
+ window.bufferScale,
+ cursorTexture ?? null,
+ window.cursorPoint.x * window.bufferScale,
+ window.cursorPoint.y * window.bufferScale,
+ this._cursorScale,
stream
).then(() => {
stream.close(null);
return Clutter.EVENT_STOP;
}
+ if (symbol === Clutter.KEY_p || symbol === Clutter.KEY_P) {
+ this._showPointerButton.checked = !this._showPointerButton.checked;
+ return Clutter.EVENT_STOP;
+ }
+
if (symbol === Clutter.KEY_Left || symbol === Clutter.KEY_Right ||
symbol === Clutter.KEY_Up || symbol === Clutter.KEY_Down) {
let direction;
gboolean include_frame;
float scale;
+ ClutterContent *cursor_content;
+ graphene_point_t cursor_point;
+ float cursor_scale;
};
G_DEFINE_TYPE_WITH_PRIVATE (ShellScreenshot, shell_screenshot, G_TYPE_OBJECT);
float scale;
g_autoptr (GError) error = NULL;
g_autoptr (ClutterContent) content = NULL;
+ MetaCursorTracker *tracker;
+ CoglTexture *cursor_texture;
+ int cursor_hot_x, cursor_hot_y;
display = shell_global_get_display (priv->global);
meta_display_get_size (display, &width, &height);
return;
}
+ tracker = meta_cursor_tracker_get_for_display (display);
+ cursor_texture = meta_cursor_tracker_get_sprite (tracker);
+
+ // If the cursor is invisible, the texture is NULL.
+ if (cursor_texture)
+ {
+ unsigned int width, height;
+ CoglContext *ctx;
+ CoglPipeline *pipeline;
+ CoglTexture2D *texture;
+ CoglOffscreen *offscreen;
+ ClutterStageView *view;
+
+ // Copy the texture to prevent it from changing shortly after.
+ width = cogl_texture_get_width (cursor_texture);
+ height = cogl_texture_get_height (cursor_texture);
+
+ ctx = clutter_backend_get_cogl_context (clutter_get_default_backend ());
+
+ texture = cogl_texture_2d_new_with_size (ctx, width, height);
+ offscreen = cogl_offscreen_new_with_texture (texture);
+ cogl_framebuffer_clear4f (COGL_FRAMEBUFFER (offscreen),
+ COGL_BUFFER_BIT_COLOR,
+ 0, 0, 0, 0);
+
+ pipeline = cogl_pipeline_new (ctx);
+ cogl_pipeline_set_layer_texture (pipeline, 0, cursor_texture);
+
+ cogl_framebuffer_draw_textured_rectangle (COGL_FRAMEBUFFER (offscreen),
+ pipeline,
+ -1, 1, 1, -1,
+ 0, 0, 1, 1);
+ cogl_object_unref (pipeline);
+ g_object_unref (offscreen);
+
+ priv->cursor_content =
+ clutter_texture_content_new_from_texture (texture, NULL);
+ cogl_object_unref (texture);
+
+ priv->cursor_scale = meta_cursor_tracker_get_scale (tracker);
+
+ meta_cursor_tracker_get_pointer (tracker, &priv->cursor_point, NULL);
+
+ view = clutter_stage_get_view_at (stage,
+ priv->cursor_point.x,
+ priv->cursor_point.y);
+
+ meta_cursor_tracker_get_hot (tracker, &cursor_hot_x, &cursor_hot_y);
+ priv->cursor_point.x -= cursor_hot_x * priv->cursor_scale;
+ priv->cursor_point.y -= cursor_hot_y * priv->cursor_scale;
+
+ // Align the coordinates to the pixel grid the same way it's done in
+ // MetaCursorRenderer.
+ if (view)
+ {
+ cairo_rectangle_int_t view_layout;
+ float view_scale;
+
+ clutter_stage_view_get_layout (view, &view_layout);
+ view_scale = clutter_stage_view_get_scale (view);
+
+ priv->cursor_point.x -= view_layout.x;
+ priv->cursor_point.y -= view_layout.y;
+
+ priv->cursor_point.x =
+ floorf (priv->cursor_point.x * view_scale) / view_scale;
+ priv->cursor_point.y =
+ floorf (priv->cursor_point.y * view_scale) / view_scale;
+
+ priv->cursor_point.x += view_layout.x;
+ priv->cursor_point.y += view_layout.y;
+ }
+ }
+
g_task_return_pointer (result, g_steal_pointer (&content), g_object_unref);
}
* @screenshot: the #ShellScreenshot
* @result: the #GAsyncResult that was provided to the callback
* @scale: (out) (optional): location to store the content scale
+ * @cursor_content: (out) (optional): location to store the cursor content
+ * @cursor_point: (out) (optional): location to store the point at which to
+ * draw the cursor content
+ * @cursor_scale: (out) (optional): location to store the cursor scale
* @error: #GError for error reporting
*
* Finish the asynchronous operation started by
*
*/
ClutterContent *
-shell_screenshot_screenshot_stage_to_content_finish (ShellScreenshot *screenshot,
- GAsyncResult *result,
- float *scale,
- GError **error)
+shell_screenshot_screenshot_stage_to_content_finish (ShellScreenshot *screenshot,
+ GAsyncResult *result,
+ float *scale,
+ ClutterContent **cursor_content,
+ graphene_point_t *cursor_point,
+ float *cursor_scale,
+ GError **error)
{
ShellScreenshotPrivate *priv = screenshot->priv;
ClutterContent *content;
if (scale)
*scale = priv->scale;
+ if (cursor_content)
+ *cursor_content = g_steal_pointer (&priv->cursor_content);
+ else
+ g_clear_pointer (&priv->cursor_content, g_object_unref);
+
+ if (cursor_point)
+ *cursor_point = priv->cursor_point;
+
+ if (cursor_scale)
+ *cursor_scale = priv->cursor_scale;
+
return content;
}
* @y: y coordinate of the rectangle
* @width: width of the rectangle, or -1 to use the full texture
* @height: height of the rectangle, or -1 to use the full texture
+ * @scale: scale of the source texture
+ * @cursor: (nullable): the cursor texture
+ * @cursor_x: x coordinate to put the cursor texture at, relative to the full
+ * source texture
+ * @cursor_y: y coordinate to put the cursor texture at, relative to the full
+ * source texture
+ * @cursor_scale: scale of the cursor texture
* @stream: the stream to write the PNG image into
* @callback: (scope async): function to call returning success or failure
* @user_data: the data to pass to callback function
int y,
int width,
int height,
+ float scale,
+ CoglTexture *cursor,
+ int cursor_x,
+ int cursor_y,
+ float cursor_scale,
GOutputStream *stream,
GAsyncReadyCallback callback,
gpointer user_data)
CoglContext *ctx;
CoglTexture *sub_texture;
cairo_surface_t *surface;
+ cairo_surface_t *cursor_surface;
+ cairo_t *cr;
g_autoptr (GTask) task = NULL;
g_autoptr (GdkPixbuf) pixbuf = NULL;
g_autofree char *creation_time = NULL;
cogl_object_unref (sub_texture);
+ cairo_surface_set_device_scale (surface, scale, scale);
+
+ if (cursor != NULL)
+ {
+ // Paint the cursor on top.
+ cursor_surface =
+ cairo_image_surface_create (CAIRO_FORMAT_ARGB32,
+ cogl_texture_get_width (cursor),
+ cogl_texture_get_height (cursor));
+ cogl_texture_get_data (cursor, CLUTTER_CAIRO_FORMAT_ARGB32,
+ cairo_image_surface_get_stride (cursor_surface),
+ cairo_image_surface_get_data (cursor_surface));
+ cairo_surface_mark_dirty (cursor_surface);
+
+ cairo_surface_set_device_scale (cursor_surface,
+ 1 / cursor_scale,
+ 1 / cursor_scale);
+
+ cr = cairo_create (surface);
+ cairo_set_source_surface (cr, cursor_surface,
+ (cursor_x - x) / scale,
+ (cursor_y - y) / scale);
+ cairo_paint (cr);
+ cairo_destroy (cr);
+
+ cairo_surface_destroy (cursor_surface);
+ }
+
// Save to an image.
pixbuf = gdk_pixbuf_get_from_surface (surface,
0, 0,
void shell_screenshot_screenshot_stage_to_content (ShellScreenshot *screenshot,
GAsyncReadyCallback callback,
gpointer user_data);
-ClutterContent *shell_screenshot_screenshot_stage_to_content_finish (ShellScreenshot *screenshot,
- GAsyncResult *result,
- float *scale,
- GError **error);
+ClutterContent *shell_screenshot_screenshot_stage_to_content_finish (ShellScreenshot *screenshot,
+ GAsyncResult *result,
+ float *scale,
+ ClutterContent **cursor_content,
+ graphene_point_t *cursor_point,
+ float *cursor_scale,
+ GError **error);
void shell_screenshot_pick_color (ShellScreenshot *screenshot,
int x,
int y,
int width,
int height,
+ float scale,
+ CoglTexture *cursor,
+ int cursor_x,
+ int cursor_y,
+ float cursor_scale,
GOutputStream *stream,
GAsyncReadyCallback callback,
gpointer user_data);